Skip to content

Conversation

@ehmeister
Copy link

@ehmeister ehmeister commented Aug 9, 2025

This PR adds integration with Cap, a lightweight proof‑of‑work CAPTCHA. It includes a new cap guard, a frontend snippet that renders the , configuration options, i18n labels, and documentation.

Why Cap?
Private and FOSS; no tracking/fingerprinting
Fast and tiny widget
Self‑hostable with simple server API
See: Cap documentation

Summary by CodeRabbit

  • New Features

    • Introduced CAPTCHA verification for forms, enhancing protection against automated submissions.
    • Added a configurable CAPTCHA widget with support for multiple languages.
    • CAPTCHA widget automatically resets when using dynamic form updates.
  • Documentation

    • Added detailed instructions for setting up and customizing the new CAPTCHA feature.
  • Translations

    • Added CAPTCHA-related translations for Czech, German, English, Spanish, French, Italian, and Dutch.

@coderabbitai
Copy link

coderabbitai bot commented Aug 9, 2025

Walkthrough

A new CAPTCHA guard named "CapGuard" has been introduced, integrating proof-of-work CAPTCHA functionality into the DreamForm system. This includes backend verification logic, configuration options, frontend widget rendering with dynamic script injection, and multilingual support. Documentation has been added to describe usage, configuration, and workflow.

Changes

Cohort / File(s) Change Summary
CapGuard Backend Implementation
classes/Guards/CapGuard.php
Introduced CapGuard class for CAPTCHA verification, including configuration access, verification logic, and availability checks.
Configuration Options
config/options.php
Added new 'cap' section under 'guards' with options for endpoint, secret key, script injection, and asset server usage.
Plugin Registration
index.php
Registered CapGuard and its snippet path within the DreamForm plugin.
Frontend CAPTCHA Widget
snippets/guards/cap.php
Added snippet rendering <cap-widget> with localized labels, dynamic script loading, and HTMX integration for widget reset.
Documentation
docs/4_guards/2_cap.md
Added documentation describing CapGuard integration, configuration, workflow, and options.
Translations
translations/en.json, translations/de.json, translations/fr.json, translations/es.json, translations/it.json, translations/nl.json, translations/cs.json
Added localized strings for CAPTCHA widget states ("verifying", "initial", "solved", "error") in multiple languages.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant CapServer
    participant DreamForm (Backend)

    User->>Browser: Loads form with <cap-widget>
    Browser->>CapServer: Requests challenge
    CapServer-->>Browser: Returns challenge
    Browser->>Browser: Solves proof-of-work, gets token
    User->>DreamForm (Backend): Submits form with CAPTCHA token
    DreamForm (Backend)->>CapServer: Verifies token (POST /verify)
    CapServer-->>DreamForm (Backend): Returns verification result
    DreamForm (Backend)->>User: Accepts or rejects submission
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Poem

A CAPTCHA hop, a bunny’s leap,
Now forms are guarded while you sleep.
With proof-of-work, bots take fright,
As humans pass with furry light.
In many tongues, the widget sings,
Secure and swift on gentle wings.
🐇✨

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 9, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0af5cf3 and 46baee1.

📒 Files selected for processing (12)
  • classes/Guards/CapGuard.php (1 hunks)
  • config/options.php (1 hunks)
  • docs/4_guards/8_cap.md (1 hunks)
  • index.php (2 hunks)
  • snippets/guards/cap.php (1 hunks)
  • translations/cs.json (1 hunks)
  • translations/de.json (1 hunks)
  • translations/en.json (1 hunks)
  • translations/es.json (1 hunks)
  • translations/fr.json (1 hunks)
  • translations/it.json (1 hunks)
  • translations/nl.json (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
index.php (2)
classes/DreamForm.php (1)
  • DreamForm (20-314)
classes/Guards/CapGuard.php (1)
  • CapGuard (9-53)
classes/Guards/CapGuard.php (4)
classes/DreamForm.php (2)
  • DreamForm (20-314)
  • option (279-287)
classes/Models/SubmissionPage.php (2)
  • SubmissionPage (37-671)
  • valueFromBody (267-280)
classes/Guards/Guard.php (1)
  • Guard (10-67)
classes/Guards/AkismetGuard.php (1)
  • post (111-127)
🪛 markdownlint-cli2 (0.17.2)
docs/4_guards/8_cap.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


7-7: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)


35-35: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)


45-45: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)


49-49: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🪛 PHPMD (2.15.0)
classes/Guards/CapGuard.php

15-15: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'endpoint'. (Clean Code Rules)

(StaticAccess)


20-20: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'secretKey'. (Clean Code Rules)

(StaticAccess)


27-27: Avoid using static access to class '\tobimori\DreamForm\Models\SubmissionPage' in method 'run'. (Clean Code Rules)

(StaticAccess)


30-32: Avoid using static access to class '\Kirby\Http\Remote' in method 'run'. (Clean Code Rules)

(StaticAccess)


30-30: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'run'. (Clean Code Rules)

(StaticAccess)

🔇 Additional comments (7)
translations/fr.json (1)

185-188: LGTM — FR i18n keys added correctly

Keys and values look consistent with other locales and the registered translation prefix.

index.php (1)

131-131: Snippet registration added

New snippet path dreamform/guards/cap registered. Make sure CSP allows loading the external script and inline JS (or provide a nonce).

Do you have a CSP in production? If so, confirm that:

  • script-src allows the chosen CDN or the asset server origin
  • unsafe-inline is avoided (or a nonce is applied to the inline HTMX handler)
translations/nl.json (1)

185-188: LGTM — NL i18n keys added correctly

Consistent wording and punctuation with the rest of the file.

translations/it.json (1)

185-188: LGTM — IT i18n keys added correctly

Consistent with other locales; no JSON issues spotted.

snippets/guards/cap.php (2)

1-57: Ensure cap-token input exists & normalize endpoint URL

Please verify the following before merging:

  • snippets/guards/cap.php
    • Confirm that <cap-widget> (from @cap.js/widget) renders a hidden input with name="cap-token", since CapGuard::run() reads SubmissionPage::valueFromBody('cap-token').
  • classes/Guards/CapGuard.php
    • Normalize the configured endpoint to always include a trailing slash before appending "siteverify" (e.g. rtrim($endpoint, '/') . '/siteverify') to avoid malformed URLs when the option lacks a slash.
    • (Optional) Harden error handling around Remote::post(), covering network failures, non-JSON responses, and missing success keys.

Let me know if you’d like me to open a follow-up PR to implement endpoint normalization and stronger error handling.


39-39: Prefer ESM loader with defer and guard against double‐loads

Update the script injection in snippets/guards/cap.php to load the widget as an ES module (where supported), defer execution, and prevent re-initialization if the snippet appears multiple times:

• File: snippets/guards/cap.php
• Location: the <script src="<?= $scriptSrc ?>"></script> injection

-    <script src="<?= $scriptSrc ?>"></script>
+    <script type="module" src="<?= $scriptSrc ?>" defer></script>
+    <script>
+      // Prevent multiple initializations when rendering the snippet more than once
+      if (typeof window !== 'undefined' && !window.__dreamformCapLoaded) {
+        window.__dreamformCapLoaded = true;
+      }
+    </script>

If @cap.js/widget isn’t distributed as an ES module, drop the type="module" attribute. Please confirm in the widget’s documentation.

classes/Guards/CapGuard.php (1)

15-21: About PHPMD StaticAccess warnings

Static access here follows established Kirby patterns (DreamForm::option, Remote::post, SubmissionPage::valueFromBody). Unless there’s a project-wide move away from these, consider suppressing these for this guard to avoid noise.

Do you want these StaticAccess warnings suppressed (e.g., per-file PHPMD rule set), or should we refactor to instance-based access across the guards for consistency?

Also applies to: 27-27, 30-30

Comment on lines +30 to +32
$remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [
'data' => $data
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Build verification URL robustly and add timeout

Be resilient to missing/trailing slashes and set a reasonable timeout to avoid hanging requests.

-		$remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [
-			'data' => $data
-		]);
+		$endpoint = static::endpoint();
+		$verifyUrl = rtrim((string)$endpoint, '/') . '/siteverify';
+		$remote = Remote::post($verifyUrl, [
+			'data' => $data,
+			'timeout' => 5,
+		]);

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 PHPMD (2.15.0)

30-32: Avoid using static access to class '\Kirby\Http\Remote' in method 'run'. (Clean Code Rules)

(StaticAccess)


30-30: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'run'. (Clean Code Rules)

(StaticAccess)

🤖 Prompt for AI Agents
In classes/Guards/CapGuard.php around lines 30 to 32, the construction of the
verification URL does not handle missing or trailing slashes robustly and lacks
a timeout setting for the HTTP request. Fix this by ensuring the base URL and
endpoint are concatenated properly with exactly one slash between them,
regardless of trailing slashes. Also, add a timeout parameter to the
Remote::post call to prevent the request from hanging indefinitely.

Comment on lines +34 to +41
$result = $remote->json();

if (
$result['success'] !== true
) {
$this->cancel(t('dreamform.submission.error.captcha'));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against network/JSON failures and undefined indexes

Remote::post/json() can fail or return non-array; accessing $result['success'] directly risks warnings. Handle HTTP errors and parse safely.

-		$result = $remote->json();
-
-		if (
-			$result['success'] !== true
-		) {
-			$this->cancel(t('dreamform.submission.error.captcha'));
-		}
+		$result = $remote->json();
+
+		$ok = method_exists($remote, 'code') ? ($remote->code() >= 200 && $remote->code() < 400) : true;
+		$success = is_array($result) ? ($result['success'] ?? false) === true : false;
+
+		if (!$ok || !$success) {
+			$this->cancel(t('dreamform.submission.error.captcha'));
+			return;
+		}
🤖 Prompt for AI Agents
In classes/Guards/CapGuard.php around lines 34 to 41, the code directly accesses
$result['success'] without checking if $result is a valid array or if the HTTP
request succeeded, which can cause warnings or errors. Modify the code to first
verify that the HTTP response was successful and that $result is a valid array
before accessing 'success'. Add error handling for network failures and JSON
parsing issues, and only call $this->cancel if the 'success' key exists and is
not true.

Comment on lines 41 to 56
<?php if (Htmx::isActive()) : ?>
<script>
// HTMX handlers for reset only
if (typeof htmx !== 'undefined') {
htmx.on("htmx:beforeSwap", () => {
// Reset the widget if needed
const el = document.querySelector("cap-widget");
if (el) {
// Remove and recreate the widget for a clean reset
const newEl = el.cloneNode(true);
el.parentNode.replaceChild(newEl, el);
}
});
}
</script>
<?php endif; ?>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

HTMX reset: scope to the current widget and handle multi-widget pages

Current code resets the first cap-widget on every htmx:beforeSwap. Scope it to this form’s widget and handle multiple widgets safely.

-        if (typeof htmx !== 'undefined') {
-            htmx.on("htmx:beforeSwap", () => {
-                // Reset the widget if needed
-                const el = document.querySelector("cap-widget");
-                if (el) {
-                    // Remove and recreate the widget for a clean reset
-                    const newEl = el.cloneNode(true);
-                    el.parentNode.replaceChild(newEl, el);
-                }
-            });
-        }
+        if (typeof htmx !== 'undefined') {
+            htmx.on("htmx:beforeSwap", (evt) => {
+                const el = document.getElementById("cap-<?= $form->uid() ?>");
+                if (!el) return;
+                // Only reset if the swap targets a container that contains this widget
+                const target = evt?.detail?.target || evt?.target || document;
+                if (target && target.contains(el)) {
+                    const newEl = el.cloneNode(true);
+                    el.parentNode.replaceChild(newEl, el);
+                }
+            });
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<?php if (Htmx::isActive()) : ?>
<script>
// HTMX handlers for reset only
if (typeof htmx !== 'undefined') {
htmx.on("htmx:beforeSwap", () => {
// Reset the widget if needed
const el = document.querySelector("cap-widget");
if (el) {
// Remove and recreate the widget for a clean reset
const newEl = el.cloneNode(true);
el.parentNode.replaceChild(newEl, el);
}
});
}
</script>
<?php endif; ?>
<?php if (Htmx::isActive()) : ?>
<script>
// HTMX handlers for reset only
if (typeof htmx !== 'undefined') {
htmx.on("htmx:beforeSwap", (evt) => {
const el = document.getElementById("cap-<?= $form->uid() ?>");
if (!el) return;
// Only reset if the swap targets a container that contains this widget
const target = evt?.detail?.target || evt?.target || document;
if (target && target.contains(el)) {
const newEl = el.cloneNode(true);
el.parentNode.replaceChild(newEl, el);
}
});
}
</script>
<?php endif; ?>
🤖 Prompt for AI Agents
In snippets/guards/cap.php between lines 41 and 56, the HTMX reset logic targets
only the first cap-widget globally, which can cause issues on pages with
multiple widgets. Modify the event handler to scope the reset to the widget
related to the current HTMX request by using the event target or closest form
element, and ensure it safely handles multiple widgets by resetting only the
relevant one instead of the first found globally.

Comment on lines +185 to +188
"guards.cap.i18n.verifying": "Ověřování...",
"guards.cap.i18n.initial": "Jsem člověk",
"guards.cap.i18n.solved": "Jsem člověk",
"guards.cap.i18n.error": "Chyba",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Czech CAP i18n keys added — minor UX nit

Keys are consistent. Consider using a distinct “solved” string (e.g., “Ověřeno”) to make state changes clearer for screen readers, instead of repeating “Jsem člověk”.

🤖 Prompt for AI Agents
In translations/cs.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently uses the same string as "guards.cap.i18n.initial" ("Jsem člověk"),
which can confuse screen readers. Update the "guards.cap.i18n.solved" value to a
distinct string like "Ověřeno" to clearly indicate the solved state and improve
accessibility.

Comment on lines +185 to +188
"guards.cap.i18n.verifying": "Verifizieren...",
"guards.cap.i18n.initial": "Ich bin ein Mensch",
"guards.cap.i18n.solved": "Ich bin ein Mensch",
"guards.cap.i18n.error": "Fehler",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

German CAP i18n keys added — minor UX nit

All good. You may want a distinct solved label (e.g., “Verifiziert”) rather than repeating “Ich bin ein Mensch” to improve a11y clarity.

🤖 Prompt for AI Agents
In translations/de.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently duplicates the "Ich bin ein Mensch" label used for
"guards.cap.i18n.initial". To improve accessibility clarity, change the
"guards.cap.i18n.solved" value to a distinct label such as "Verifiziert" to
clearly differentiate the solved state from the initial state.

Comment on lines +185 to +188
"guards.cap.i18n.verifying": "Verifying...",
"guards.cap.i18n.initial": "I'm a human",
"guards.cap.i18n.solved": "I'm a human",
"guards.cap.i18n.error": "Error",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

English CAP i18n keys added — minor UX nit

LGTM. Consider changing the solved label to “Verified” (or similar) so the state change is obvious to screen readers and visual users.

🤖 Prompt for AI Agents
In translations/en.json around lines 185 to 188, the "guards.cap.i18n.solved"
key currently has the value "I'm a human," which is not clear for indicating the
solved state. Change the value of "guards.cap.i18n.solved" to "Verified" or a
similar term that clearly communicates the state change to both screen readers
and visual users.

Comment on lines +185 to +188
"guards.cap.i18n.verifying": "Verificando...",
"guards.cap.i18n.initial": "Soy un humano",
"guards.cap.i18n.solved": "Soy un humano",
"guards.cap.i18n.error": "Error",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Spanish CAP i18n keys added — minor UX nit

Looks good. Optionally differentiate the solved label (e.g., “Verificado”) instead of repeating “Soy un humano” to better convey state change for assistive tech.

🤖 Prompt for AI Agents
In translations/es.json around lines 185 to 188, the "guards.cap.i18n.solved"
key repeats the label "Soy un humano" which is the same as
"guards.cap.i18n.initial". To improve UX and assistive technology clarity,
change the "solved" label to a distinct term like "Verificado" to clearly
indicate the state change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46baee1 and 88414ca.

📒 Files selected for processing (1)
  • docs/4_guards/2_cap.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/4_guards/2_cap.md

[grammar] ~6-~6: Use correct spacing
Context: .... It’s fast, self-hostable, and easy to integrate. ## Adding Cap Follow the official Cap docs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~8-~8: Use correct spacing
Context: ...able, and easy to integrate. ## Adding Cap Follow the official Cap docs to deploy a...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~10-~10: Use correct spacing
Context: ... the guard and required options to your config.php. php // site/config/config.php return [ 'tobimori.dreamform' => [ 'guards' => [ 'available' => ['cap', /* other guards here */], 'cap' => [ // Base API endpoint of your Cap server (https://<instance_url>/<site_key>/) 'endpoint' => fn () => env('CAP_ENDPOINT'), // Server secret used to verify the token server-to-server 'secretKey' => fn () => env('CAP_SECRET_KEY'), // Automatically inject the widget script 'injectScript' => true, // Load widget from your Cap server's asset endpoint instead of the CDN 'useAssetServer' => false, ], ], ], ]; Ideally, you should not commit these key...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~34-~34: Use commas correctly
Context: ...ou should not commit these keys to your repository, but instead load them from environment v...

(QB_NEW_EN_OTHER_ERROR_IDS_33)


[grammar] ~34-~34: Use a comma after introductory words or phrases
Context: ...d load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno...

(QB_NEW_EN_OTHER_ERROR_IDS_19)


[grammar] ~34-~34: Use correct spacing
Context: ...kirby3-dotenv), as shown in the example above. ## How it works - When a form includes the...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~36-~36: Use correct spacing
Context: ... shown in the example above. ## How it works - When a form includes the cap guard, Dr...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~40-~40: Use correct spacing
Context: ...ver using the configured endpoint and secretKey. Cap supports customization via CSS varia...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~44-~44: Use correct spacing
Context: ...ts the widget between swaps for a clean state. ## Options | Option | Default | Accepts | ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~51-~51: There might be a mistake here.
Context: ... to verify tokens with your Cap server. | | tobimori.dreamform.guards.cap.injectScr...

(QB_NEW_EN_OTHER)


[grammar] ~53-~53: There might be a mistake here.
Context: ...t.js` based on the configured endpoint. |

(QB_NEW_EN_OTHER)

🪛 markdownlint-cli2 (0.17.2)
docs/4_guards/2_cap.md

50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


51-51: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🔇 Additional comments (1)
docs/4_guards/2_cap.md (1)

44-45: Scope the HTMX auto-reset note

Clarify whether the auto-reset relies on the injected inline script from the snippet (and thus requires injectScript = true) or works even when injectScript = false and users include their own script.

If it requires the snippet’s inline script, explicitly note that requirement here.

@@ -0,0 +1,53 @@
---
title: Cap
description: Cap proof-of-work captcha integration
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Capitalize “CAPTCHA” for consistency with the rest of the doc

Use consistent capitalization across title/description/body.

-description: Cap proof-of-work captcha integration
+description: Cap proof-of-work CAPTCHA integration
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Cap proof-of-work captcha integration
description: Cap proof-of-work CAPTCHA integration
🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 3, the word "captcha" in the description
should be capitalized as "CAPTCHA" to maintain consistency with the rest of the
document. Update the description line to use "CAPTCHA" instead of "captcha".

Comment on lines +21 to +28
'endpoint' => fn () => env('CAP_ENDPOINT'),
// Server secret used to verify the token server-to-server
'secretKey' => fn () => env('CAP_SECRET_KEY'),
// Automatically inject the widget script
'injectScript' => true,
// Load widget from your Cap server's asset endpoint instead of the CDN
'useAssetServer' => false,
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify endpoint format and script loading behavior

  • Endpoint: explicitly state it must include the site key and a trailing slash, e.g. https://example.com/abcd1234/. That avoids common misconfig issues when deriving other URLs.
  • Script loading: clarify precedence and behavior:
    • If injectScript = true and useAssetServer = true, the widget JS is loaded from the asset server derived from endpoint; otherwise from CDN.
    • If injectScript = false, users must include the widget script manually and ensure the inline snippet behavior (e.g., HTMX reset) is also included, if required.

Please confirm the actual runtime precedence used by CapGuard/snippet so the docs can match it precisely.


🏁 Script executed:

#!/bin/bash
# Search for injectScript configuration references and implementation
rg -n "injectScript" -A5 -B5

# Search for useAssetServer configuration references and implementation
rg -n "useAssetServer" -A5 -B5

Length of output: 10107


🏁 Script executed:

#!/bin/bash
# Display the full injection logic in the Cap guard snippet for precise behavior
rg -n "if (DreamForm::option('guards.cap.injectScript')" -A20 -B5 snippets/guards/cap.php

Length of output: 200


🏁 Script executed:

#!/bin/bash
# Show the full Cap guard snippet to confirm script injection logic
sed -n '1,200p' snippets/guards/cap.php

Length of output: 2034


Clarify CAP endpoint format and widget‐script loading
Please update docs/4_guards/2_cap.md to match the actual runtime logic you’ve verified:

  • Endpoint

    • Make it clear that this value must include your site key (and it’s safest to include a trailing slash), e.g.
      // Base API endpoint of your Cap server (including site key and trailing slash):
      //   https://example.com/abcd1234/
      'endpoint' => fn() => env('CAP_ENDPOINT'),
    • This full URL is passed into the <cap-widget> via data-cap-api-endpoint and used by the client library to build its own URLs.
  • injectScript (lines 24–25)

    • When true, the widget’s <script src="…"> is injected automatically; when false, nothing is injected and you must include both the <script> tag and the inline HTMX reset snippet yourself.
    • Default: true
    • Fallback (useAssetServer = false): loads from CDN:
      https://cdn.jsdelivr.net/npm/@cap.js/widget
  • useAssetServer (line 26)

    • Only takes effect when injectScript = true.
    • When true, the script is loaded from your Cap server’s asset endpoint at
      https://<host>[:port]/assets/widget.js
      
      (the code parses only the scheme/host/port from your endpoint and appends /assets/widget.js)
    • Default: false

Locations to update:

  • docs/4_guards/2_cap.md around lines 20–28 (sample config block and prose)
  • Table rows at lines 50–53 to reflect this exact precedence and the correct /assets/widget.js path
🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md around lines 20 to 28 and 50 to 53, update the
documentation to clarify that the 'endpoint' value must include the full URL
with the site key and a trailing slash, e.g., https://example.com/abcd1234/, as
this URL is passed to the <cap-widget> and used by the client library. For
'injectScript' (lines 24-25), specify that when true, the widget script is
injected automatically, and when false, the user must include both the script
tag and the HTMX reset snippet manually; note the default is true and fallback
loads from the CDN URL https://cdn.jsdelivr.net/npm/@cap.js/widget. For
'useAssetServer' (line 26), clarify it only applies when injectScript is true,
and when true, the script loads from the Cap server's asset endpoint at
https://<host>[:port]/assets/widget.js, derived from the scheme/host/port of the
endpoint; default is false. Also update the table rows at lines 50-53 to reflect
this precedence and the exact /assets/widget.js path.

];
```

Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Minor grammar polish: “e.g., …”

Add a comma after “e.g.” per style guides.

-Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
+Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g., using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g., using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above.
🧰 Tools
🪛 LanguageTool

[grammar] ~34-~34: Use commas correctly
Context: ...ou should not commit these keys to your repository, but instead load them from environment v...

(QB_NEW_EN_OTHER_ERROR_IDS_33)


[grammar] ~34-~34: Use a comma after introductory words or phrases
Context: ...d load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno...

(QB_NEW_EN_OTHER_ERROR_IDS_19)


[grammar] ~34-~34: Use correct spacing
Context: ...kirby3-dotenv), as shown in the example above. ## How it works - When a form includes the...

(QB_NEW_EN_OTHER_ERROR_IDS_5)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 34, add a comma after "e.g." to correct the
grammar and follow style guides. Change "e.g using" to "e.g., using" to improve
readability and correctness.

Comment on lines +48 to +53
| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix table rendering (MD056) and a broken URL

  • The “Accepts” cells include a pipe character, which many linters/renderers interpret as a column separator. Escape the pipe.
  • Fix the triple slash in the asset URL.

These currently break table parsing and show an invalid URL.

-| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
-| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
+| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
+| tobimori.dreamform.guards.cap.secretKey | `null` | `string\|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
-| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
+| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
-| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
+| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https://<instance_url>/widget.js` based on the configured endpoint. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.secretKey | `null` | `string\|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https://<instance_url>/widget.js` based on the configured endpoint. |
🧰 Tools
🪛 LanguageTool

[grammar] ~51-~51: There might be a mistake here.
Context: ... to verify tokens with your Cap server. | | tobimori.dreamform.guards.cap.injectScr...

(QB_NEW_EN_OTHER)


[grammar] ~53-~53: There might be a mistake here.
Context: ...t.js` based on the configured endpoint. |

(QB_NEW_EN_OTHER)

🪛 markdownlint-cli2 (0.17.2)

50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


51-51: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md around lines 48 to 53, the table's "Accepts" column
contains unescaped pipe characters that break markdown table rendering, and the
URL in the "useAssetServer" description has an incorrect triple slash. Fix this
by escaping the pipe characters in the "Accepts" cells (e.g., replace | with \|)
and correct the URL by changing the triple slash to a double slash to form a
valid URL.


| Option | Default | Accepts | Description |
| --- | --- | --- | --- |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Nit: remove the stray space inside parentheses

Small formatting tidy-up in the description cell.

-| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
+| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

50-50: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In docs/4_guards/2_cap.md at line 50, remove the extra space inside the
parentheses in the description cell so that the URL example reads
`https://<instance_url>/<site_key>/` without any leading or trailing spaces
inside the parentheses.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
snippets/guards/cap.php (3)

12-18: Make widget id unique per form instance

A static id will collide on pages with multiple forms/widgets. Use a stable, per-form id.

-    'id' => 'cap',
+    // Unique per form to avoid collisions on multi-form pages
+    'id' => 'cap-' . $form->uid(),

22-33: Pin CDN version and bracket IPv6 hosts when reconstructing asset server origin

  • Pin a specific version to avoid unexpected breaking changes.
  • When deriving the asset server URL from the endpoint, wrap IPv6 hosts in brackets.
-    $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/widget';
+    // Pin to a specific version to avoid accidental breaking changes
+    $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/[email protected]';
@@
-                $serverUrl = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '');
+                $host = $parts['host'];
+                // Bracket IPv6 hosts
+                if (strpos($host, ':') !== false && $host[0] !== '[') {
+                    $host = '[' . $host . ']';
+                }
+                $serverUrl = $parts['scheme'] . '://' . $host . (isset($parts['port']) ? ':' . $parts['port'] : '');

To validate available versions and choose the pin, run:

#!/bin/bash
set -euo pipefail
# Show latest dist-tag and a sample of available versions for @cap.js/widget
curl -s https://registry.npmjs.org/@cap.js%2Fwidget | jq -r '
  "latest=\(.["dist-tags"].latest)",
  "versions_sample=\((.versions | keys | sort) | (.[-10:] // .) | join(", "))"
'

38-46: Scope HTMX reset to this widget only and guard DOM operations

Current handler resets the first cap-widget globally. Scope to this form’s widget and ensure node exists before replacing.

-        if (typeof htmx !== 'undefined') {
-            htmx.on("htmx:beforeSwap", () => {
-                const el = document.querySelector("cap-widget");
-                if (el) {
-                    const newEl = el.cloneNode(true);
-                    el.parentNode.replaceChild(newEl, el);
-                }
-            });
-        }
+        if (typeof htmx !== 'undefined') {
+            htmx.on("htmx:beforeSwap", (evt) => {
+                const el = document.getElementById("cap-<?= $form->uid() ?>");
+                if (!el) return;
+                // Only reset if the swap targets a container that contains this widget
+                const target = evt?.detail?.target || evt?.target || document;
+                if (target && target.contains(el) && el.parentNode) {
+                    const newEl = el.cloneNode(true);
+                    el.parentNode.replaceChild(newEl, el);
+                }
+            });
+        }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88414ca and bb4cae3.

📒 Files selected for processing (1)
  • snippets/guards/cap.php (1 hunks)
🔇 Additional comments (2)
snippets/guards/cap.php (2)

12-18: i18n data attributes look correct

data-cap-i18n-initial-state is the documented key; others match the widget API.


36-47: Verify CSP configuration
I didn’t find any Content-Security-Policy headers or nonce usage in the codebase. Please confirm whether your site enforces a CSP with script-src restrictions. If it does, consider moving this inline HTMX handler into a bundled asset or injecting a CSP nonce.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants